<?php
/**
 * Simple PHP Contact Form
 *
 * This file is part of the Simple PHP Contact Form which is subject to the New
 * BSD License {@see LICENSE} which you would be advised to read before using,
 * modifying or redistributing this software.
 *
 * PHP version 5.2
 *
 * @category  Simple-PHP-Contact-Form
 * @package   Application_Controllers
 * @author    jah <jah@jahboite.co.uk>
 * @copyright 2010 jah <jah@jahboite.co.uk>
 * @license   New BSD License {@see LICENSE}
 * @version   SVN: $Id$
 * @link      http://code.google.com/p/simple-php-contact-form/
 */


/**
 * IndexController.
 *
 * @category  Simple-PHP-Contact-Form
 * @package   Application_Controllers
 * @author    jah <jah@jahboite.co.uk>
 * @copyright 2010 jah <jah@jahboite.co.uk>
 * @license   New BSD License {@see LICENSE}
 * @version   Release: @package_version@
 * @link      http://code.google.com/p/simple-php-contact-form/
 */
class IndexController extends Zend_Controller_Action
{
    private $_recipientsConfig = null;
    private $_formConfig       = null;
    private $_form             = null;
    private $_auditor          = null;

    /**
     * Initialises the form at each request of this controller.
     * TODO error action doesn't need init - so either move the error action
     * or move the init stuff elsewhere.
     *
     * @return null
     */
    public function init()
    {
        // Get the form configuration data.
        $bs = $this->getInvokeArg('bootstrap');
        $formConfigLoc = $bs->getOption('formconfigloc');
        if ($formConfigLoc === null) {
            throw new EnterpriseSecurityException(
                'Your request could not be fulfilled.',
                'Contact Application cannot start, cannot find form configuration data!'
            );
        }
        $this->_formConfig = new Zend_Config_Ini(
            $formConfigLoc, APPLICATION_ENV, false
        );
        if (! $this->_formConfig instanceof Zend_Config) {
            throw new EnterpriseSecurityException(
                'Your request could not be fulfilled.',
                'Contact Application cannot start, cannot find form configuration data!'
            );
        }

        // Get the recipients configuration data.
        $rcptConfigLoc = $bs->getOption('recipientsconfigloc');
        if (! empty($rcptConfigLoc)) {
            $this->_recipientsConfig = new Zend_Config_Ini(
                $rcptConfigLoc, APPLICATION_ENV, false
            );
        }
        if (! $this->_recipientsConfig instanceof Zend_Config) {
            $this->_auditor = ESAPI::getAuditor('IndexController');
            $this->_auditor->warning(
                Auditor::SECURITY,
                false,
                'Contact Application cannot find contact recipients data. Form submissions will be logged to file!'
            );
        }

        // get a new form and pass recipient config data to it.
        $this->_form = $this->_helper->formLoader('contact', $this->_formConfig);
        $this->_form->setRecipientConfiguration($this->_recipientsConfig);
        $this->_form->setCaptcha();

    }


    /**
     * The index action will add a form which will be rendered by the view.
     * If the form has been previously submitted and the form contained
     * validation errors then client will be redirected here and the form
     * will be retrieved from the session storage.
     *
     * @return null
     */
    public function indexAction()
    {
        // see if the form has already been submitted. if it was and it failed
        // validation then the form will be persisted in the session and can be
        // represented to the client.  if it passed validation the client can be
        // shown a thank you message.
        if (Zend_Session::sessionExists()) {

            $ns = new Zend_Session_Namespace('Contact');

            if ($ns->submission === false) {

                $this->view->contactForm = $ns->form;
                unset($ns->form);
                unset($ns->submission);
                return;

            } else if ($ns->submission === true) {

                unset($ns->submission);
                $this->render('thanks');
                return;

            }
        }

        // new form
        $this->_form->setCSRFToken();
        $this->view->contactForm = $this->_form;
    }


    /**
     * The send action is the target for form submission.  If the request does
     * not contain POST data then the response will be a redirect to the index
     * action where the form will be displayed.
     * POST data is validated and if successful an email will be sent before
     * redirecting the client away from the send action (to a success message).
     * If validation fails, the form is persisted in the session and the client
     * is redirected away from the send action to display the form with error
     * messages.
     * Whether or not the form is successfully validated, a user session will be
     * started so that the client can be tracked and certain thresholds enforced.
     *
     * @return null
     */
    public function sendAction()
    {
        if (! $this->_request->isPost()) {
            $this->_helper->getHelper('redirector')
                ->setCode(303)
                ->gotoSimple(
                    'index', null, null, $this->_request->getParams()
                );
        }

        $ns = new Zend_Session_Namespace('Contact');
        $ids = ESAPI::getIntrusionDetector();

        // Is the form submission a valid one?
        $valid = $this->_form->isValid($this->_request->getPost());

        // Check whether a csrf token is being re-used.  This should not happen
        // often due to the POST-Redirect-GET design of this application.
        // If the token is being reused then throw an Intrusion Exception.
        if (   isset($ns->history->token)
            && in_array($this->_form->token->getValue(), $ns->history->token)
        ) {
            throw new IntrusionException(
                'Form resubmission is not permitted.',
                'Submitted token is one contained in the history of the current session.'
            );
        }

        if ($valid !== true) {

            // Add a formValidationErrors event to the client session.
            $ids->addEvent(
                'ValidationErrorEvent',
                'Form submission contained inputs that caused Validation errors.'
            );

            // There are certain validation errors that require an exception to
            // be thrown becuase errors for these elements is assumed to be
            // evidence of tampering. Throw IntrusionException if there are
            // errors in CSRFToken or recipientsMap.
            if ($this->_form->detectedTamper()) {
                throw new IntrusionException(
                    'The submitted form contained invalid information.',
                    'Form submission contained evidence of tampering!'
                );
            }

            // Log a warning about failed validation and include the error
            // messages.
            $validationErrMsgs = $this->_form->getMessages(null, true);
            $vmsgs = '';
            foreach ($validationErrMsgs as $elem => $messageArray) {
                $vmsgs .= "[{$elem}:";
                foreach ($messageArray as $validator => $message) {
                    $vmsgs .= "{$validator}={$message};";
                }
                $vmsgs .= "] ";
            }
            ESAPI::getAuditor('IndexController')->warning(
                Auditor::SECURITY,
                false,
                'Validation failure messages: ' . $vmsgs
            );
            // Store the form in the session so that we can perform a redirect
            // away from the send action. // TODO expire it soon!
            $ns->submission = false;
            $ns->form = $this->_form;
            $this->_helper->getHelper('redirector')
                ->setCode(303)
                ->gotoSimple(
                    'index', null, null, $this->_request->getParams()
                );
            return;
        }

        // We have a valid form!!

        // Kill the CSRF cookie.
        setcookie(Form_Contact::CSRFCOOKIE, 'expired', 1, '/');

        // Add token to map of submitted tokens
        if (is_array($ns->history->token)) {
            array_unshift($ns->history->token, $this->_form->token->getValue());
        } else {
            $ns->history->token = array($this->_form->token->getValue());
        }

        // extract the valid values from the form and send a mail
        $validValues = array();
        $elements = $this->_form->getElements();
        foreach ($elements as $key => $elem) {
            $validValues[$key] = $elem->getValue();
        }
        // attempt to send a mail
        $successfulDelivery = $this->_helper->sendMail(
            $validValues, $this->_recipientsConfig
        );

        // If mail delivery was not successful show a
        if ($successfulDelivery !== true) {

            ESAPI::getAuditor('IndexController')->warning(
                Auditor::SECURITY, false,
                'Sending of mail failed - ' . $this->_helper->sendMail->getResponse()
            );
            $ids->addEvent(
                'MailNotDeliveredEvent',
                'successfully submitted valid contact form but mail was NOT sent.'
            );
            // TODO an encrypted logfile with failed email messages?
            // View helpful error message.
            throw new Exception('Successful form submission failed to be sent.');

        } else {

            $ns->submission = true;
            $this->_helper->getHelper('redirector')
                ->setCode(303)
                ->gotoSimple(
                    'index', null, null, $this->_request->getParams()
                );
            return;

        }

    }


    /**
     * The router is set to /action and this segment of the url is overloaded
     * so that /someRecipient is also valid.  This __call method is invoked
     * whenever this segment of the url is not a defined action and in these
     * cases, the $actionMethod may be a valid recipient.  These sre trapped
     * here and, if valid, the request is re-dispatched to the index action to
     * which we pass a recipient parameter.
     * If a valid recipient is not found then execution is passed to the  parent
     * __call method.
     *
     * @param string $actionMethod Url segment.
     * @param array  $args         Request arguments.
     *
     * @return null
     */
    public function __call($actionMethod, $args)
    {
        $logger = ESAPI::getAuditor('IndexController');

        // I do not anticipate this happening often...
        if (! is_string($actionMethod)) {

            return parent::__call($actionMethod, $args);

        }

        // If there's less than two recipients defined, we don't need to trap
        // usernames. ignore them.
        if ($this->_recipientsConfig->count() < 2) {

            return parent::__call($actionMethod, $args);

        }

        // Strip the trailing 'Action' from the method name.
        $method = null;
        $detectedCharacterEncoding = mb_detect_encoding($actionMethod);
        $len = mb_strlen($actionMethod, $detectedCharacterEncoding);
        if (mb_substr($actionMethod, $len-6, 6, $detectedCharacterEncoding) == 'Action' ) {
            $method = mb_substr(
                $actionMethod, 0, $len-6, $detectedCharacterEncoding
            );
        } else {
            $method = $actionMethod;
        }

        // Validate the possible recipient and, if valid, add a 'recipient'
        // request param and continue on to the indexAction of this controller.
        $recipientValidator = new Custom_Validate_ValidRecipient(
            $this->_recipientsConfig
        );
        if ($recipientValidator->isValid($method)) {
            $this->_request->setActionName('index');
            $this->_request->setParams(
                array(
                    'recipient' => $method,
                    'action' => 'index'
                )
            );
            $this->_request->setDispatched(false);
            return;
        }

        return parent::__call($actionMethod, $args);
    }


    /**
     * The error controller will redirect the client here and a generic message will
     * be displayed. Direct requests for this page will result in a redirect to
     * index action.
     *
     * @return null
     */
    public function errorAction()
    {
        if (Zend_Session::sessionExists()) {
            $ns = new Zend_Session_Namespace('Contact');
            if ($ns->error === true) {
                // Just show this view. Simples!
                unset($ns->error);
                return;
            }
        }

        $this->_helper->getHelper('redirector')
            ->setCode(303)
            ->gotoSimple(
                'index', null, null, $this->_request->getParams()
            );

    }


}

